/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jasper.compiler;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.util.ArrayList;

import org.apache.jasper.JasperException;
import org.apache.jasper.JspCompilationContext;
import org.xml.sax.SAXException;

/**
 * Class responsible for dispatching JSP parse and javac compilation errors to
 * the configured error handler.
 * 
 * This class is also responsible for localizing any error codes before they are
 * passed on to the configured error handler.
 * 
 * In the case of a Java compilation error, the compiler error message is parsed
 * into an array of JavacErrorDetail instances, which is passed on to the
 * configured error handler.
 * 
 * @author Jan Luehe
 * @author Kin-man Chung
 */
public class ErrorDispatcher {

	// Custom error handler
	private ErrorHandler errHandler;

	// Indicates whether the compilation was initiated by JspServlet or JspC
	private boolean jspcMode = false;

	/*
	 * Constructor.
	 * 
	 * @param jspcMode true if compilation has been initiated by JspC, false
	 * otherwise
	 */
	public ErrorDispatcher(boolean jspcMode) {
		// XXX check web.xml for custom error handler
		errHandler = new DefaultErrorHandler();
		this.jspcMode = jspcMode;
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param errCode Error code
	 */
	public void jspError(String errCode) throws JasperException {
		dispatch(null, errCode, null, null);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param where Error location
	 * 
	 * @param errCode Error code
	 */
	public void jspError(Mark where, String errCode) throws JasperException {
		dispatch(where, errCode, null, null);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param n Node that caused the error
	 * 
	 * @param errCode Error code
	 */
	public void jspError(Node n, String errCode) throws JasperException {
		dispatch(n.getStart(), errCode, null, null);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param errCode Error code
	 * 
	 * @param arg Argument for parametric replacement
	 */
	public void jspError(String errCode, String arg) throws JasperException {
		dispatch(null, errCode, new Object[] { arg }, null);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param where Error location
	 * 
	 * @param errCode Error code
	 * 
	 * @param arg Argument for parametric replacement
	 */
	public void jspError(Mark where, String errCode, String arg)
			throws JasperException {
		dispatch(where, errCode, new Object[] { arg }, null);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param n Node that caused the error
	 * 
	 * @param errCode Error code
	 * 
	 * @param arg Argument for parametric replacement
	 */
	public void jspError(Node n, String errCode, String arg)
			throws JasperException {
		dispatch(n.getStart(), errCode, new Object[] { arg }, null);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param errCode Error code
	 * 
	 * @param arg1 First argument for parametric replacement
	 * 
	 * @param arg2 Second argument for parametric replacement
	 */
	public void jspError(String errCode, String arg1, String arg2)
			throws JasperException {
		dispatch(null, errCode, new Object[] { arg1, arg2 }, null);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param errCode Error code
	 * 
	 * @param arg1 First argument for parametric replacement
	 * 
	 * @param arg2 Second argument for parametric replacement
	 * 
	 * @param arg3 Third argument for parametric replacement
	 */
	public void jspError(String errCode, String arg1, String arg2, String arg3)
			throws JasperException {
		dispatch(null, errCode, new Object[] { arg1, arg2, arg3 }, null);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param where Error location
	 * 
	 * @param errCode Error code
	 * 
	 * @param arg1 First argument for parametric replacement
	 * 
	 * @param arg2 Second argument for parametric replacement
	 */
	public void jspError(Mark where, String errCode, String arg1, String arg2)
			throws JasperException {
		dispatch(where, errCode, new Object[] { arg1, arg2 }, null);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param where Error location
	 * 
	 * @param errCode Error code
	 * 
	 * @param arg1 First argument for parametric replacement
	 * 
	 * @param arg2 Second argument for parametric replacement
	 * 
	 * @param arg3 Third argument for parametric replacement
	 */

	public void jspError(Mark where, String errCode, String arg1, String arg2,
			String arg3) throws JasperException {
		dispatch(where, errCode, new Object[] { arg1, arg2, arg3 }, null);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param n Node that caused the error
	 * 
	 * @param errCode Error code
	 * 
	 * @param arg1 First argument for parametric replacement
	 * 
	 * @param arg2 Second argument for parametric replacement
	 */

	public void jspError(Node n, String errCode, String arg1, String arg2)
			throws JasperException {
		dispatch(n.getStart(), errCode, new Object[] { arg1, arg2 }, null);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param n Node that caused the error
	 * 
	 * @param errCode Error code
	 * 
	 * @param arg1 First argument for parametric replacement
	 * 
	 * @param arg2 Second argument for parametric replacement
	 * 
	 * @param arg3 Third argument for parametric replacement
	 */

	public void jspError(Node n, String errCode, String arg1, String arg2,
			String arg3) throws JasperException {
		dispatch(n.getStart(), errCode, new Object[] { arg1, arg2, arg3 }, null);
	}

	/*
	 * Dispatches the given parsing exception to the configured error handler.
	 * 
	 * @param e Parsing exception
	 */
	public void jspError(Exception e) throws JasperException {
		dispatch(null, null, null, e);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param errCode Error code
	 * 
	 * @param arg Argument for parametric replacement
	 * 
	 * @param e Parsing exception
	 */
	public void jspError(String errCode, String arg, Exception e)
			throws JasperException {
		dispatch(null, errCode, new Object[] { arg }, e);
	}

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param n Node that caused the error
	 * 
	 * @param errCode Error code
	 * 
	 * @param arg Argument for parametric replacement
	 * 
	 * @param e Parsing exception
	 */
	public void jspError(Node n, String errCode, String arg, Exception e)
			throws JasperException {
		dispatch(n.getStart(), errCode, new Object[] { arg }, e);
	}

	/**
	 * Parses the given error message into an array of javac compilation error
	 * messages (one per javac compilation error line number).
	 * 
	 * @param errMsg
	 *            Error message
	 * @param fname
	 *            Name of Java source file whose compilation failed
	 * @param page
	 *            Node representation of JSP page from which the Java source
	 *            file was generated
	 * 
	 * @return Array of javac compilation errors, or null if the given error
	 *         message does not contain any compilation error line numbers
	 */
	public static JavacErrorDetail[] parseJavacErrors(String errMsg,
			String fname, Node.Nodes page) throws JasperException, IOException {

		return parseJavacMessage(errMsg, fname, page);
	}

	/*
	 * Dispatches the given javac compilation errors to the configured error
	 * handler.
	 * 
	 * @param javacErrors Array of javac compilation errors
	 */
	public void javacError(JavacErrorDetail[] javacErrors)
			throws JasperException {

		errHandler.javacError(javacErrors);
	}

	/*
	 * Dispatches the given compilation error report and exception to the
	 * configured error handler.
	 * 
	 * @param errorReport Compilation error report
	 * 
	 * @param e Compilation exception
	 */
	public void javacError(String errorReport, Exception e)
			throws JasperException {

		errHandler.javacError(errorReport, e);
	}

	// *********************************************************************
	// Private utility methods

	/*
	 * Dispatches the given JSP parse error to the configured error handler.
	 * 
	 * The given error code is localized. If it is not found in the resource
	 * bundle for localized error messages, it is used as the error message.
	 * 
	 * @param where Error location
	 * 
	 * @param errCode Error code
	 * 
	 * @param args Arguments for parametric replacement
	 * 
	 * @param e Parsing exception
	 */
	private void dispatch(Mark where, String errCode, Object[] args, Exception e)
			throws JasperException {
		String file = null;
		String errMsg = null;
		int line = -1;
		int column = -1;
		boolean hasLocation = false;

		// Localize
		if (errCode != null) {
			errMsg = Localizer.getMessage(errCode, args);
		} else if (e != null) {
			// give a hint about what's wrong
			errMsg = e.getMessage();
		}

		// Get error location
		if (where != null) {
			if (jspcMode) {
				// Get the full URL of the resource that caused the error
				try {
					file = where.getURL().toString();
				} catch (MalformedURLException me) {
					// Fallback to using context-relative path
					file = where.getFile();
				}
			} else {
				// Get the context-relative resource path, so as to not
				// disclose any local filesystem details
				file = where.getFile();
			}
			line = where.getLineNumber();
			column = where.getColumnNumber();
			hasLocation = true;
		}

		// Get nested exception
		Exception nestedEx = e;
		if ((e instanceof SAXException)
				&& (((SAXException) e).getException() != null)) {
			nestedEx = ((SAXException) e).getException();
		}

		if (hasLocation) {
			errHandler.jspError(file, line, column, errMsg, nestedEx);
		} else {
			errHandler.jspError(errMsg, nestedEx);
		}
	}

	/*
	 * Parses the given Java compilation error message, which may contain one or
	 * more compilation errors, into an array of JavacErrorDetail instances.
	 * 
	 * Each JavacErrorDetail instance contains the information about a single
	 * compilation error.
	 * 
	 * @param errMsg Compilation error message that was generated by the javac
	 * compiler
	 * 
	 * @param fname Name of Java source file whose compilation failed
	 * 
	 * @param page Node representation of JSP page from which the Java source
	 * file was generated
	 * 
	 * @return Array of JavacErrorDetail instances corresponding to the
	 * compilation errors
	 */
	private static JavacErrorDetail[] parseJavacMessage(String errMsg,
			String fname, Node.Nodes page) throws IOException, JasperException {

		ArrayList<JavacErrorDetail> errors = new ArrayList<JavacErrorDetail>();
		StringBuffer errMsgBuf = null;
		int lineNum = -1;
		JavacErrorDetail javacError = null;

		BufferedReader reader = new BufferedReader(new StringReader(errMsg));

		/*
		 * Parse compilation errors. Each compilation error consists of a file
		 * path and error line number, followed by a number of lines describing
		 * the error.
		 */
		String line = null;
		while ((line = reader.readLine()) != null) {

			/*
			 * Error line number is delimited by set of colons. Ignore colon
			 * following drive letter on Windows (fromIndex = 2). XXX Handle
			 * deprecation warnings that don't have line info
			 */
			int beginColon = line.indexOf(':', 2);
			int endColon = line.indexOf(':', beginColon + 1);
			if ((beginColon >= 0) && (endColon >= 0)) {
				if (javacError != null) {
					// add previous error to error vector
					errors.add(javacError);
				}

				String lineNumStr = line.substring(beginColon + 1, endColon);
				try {
					lineNum = Integer.parseInt(lineNumStr);
				} catch (NumberFormatException e) {
					lineNum = -1;
				}

				errMsgBuf = new StringBuffer();

				javacError = createJavacError(fname, page, errMsgBuf, lineNum);
			}

			// Ignore messages preceding first error
			if (errMsgBuf != null) {
				errMsgBuf.append(line);
				errMsgBuf.append("\n");
			}
		}

		// Add last error to error vector
		if (javacError != null) {
			errors.add(javacError);
		}

		reader.close();

		JavacErrorDetail[] errDetails = null;
		if (errors.size() > 0) {
			errDetails = new JavacErrorDetail[errors.size()];
			errors.toArray(errDetails);
		}

		return errDetails;
	}

	/**
	 * @param fname
	 * @param page
	 * @param errMsgBuf
	 * @param lineNum
	 * @return JavacErrorDetail The error details
	 * @throws JasperException
	 */
	public static JavacErrorDetail createJavacError(String fname,
			Node.Nodes page, StringBuffer errMsgBuf, int lineNum)
			throws JasperException {
		return createJavacError(fname, page, errMsgBuf, lineNum, null);
	}

	/**
	 * @param fname
	 * @param page
	 * @param errMsgBuf
	 * @param lineNum
	 * @param ctxt
	 * @return JavacErrorDetail The error details
	 * @throws JasperException
	 */
	public static JavacErrorDetail createJavacError(String fname,
			Node.Nodes page, StringBuffer errMsgBuf, int lineNum,
			JspCompilationContext ctxt) throws JasperException {
		JavacErrorDetail javacError;
		// Attempt to map javac error line number to line in JSP page
		ErrorVisitor errVisitor = new ErrorVisitor(lineNum);
		page.visit(errVisitor);
		Node errNode = errVisitor.getJspSourceNode();
		if ((errNode != null) && (errNode.getStart() != null)) {
			// If this is a scriplet node then there is a one to one mapping
			// between JSP lines and Java lines
			if (errVisitor.getJspSourceNode() instanceof Node.Scriptlet) {
				javacError = new JavacErrorDetail(fname, lineNum, errNode
						.getStart().getFile(), errNode.getStart()
						.getLineNumber()
						+ lineNum
						- errVisitor.getJspSourceNode().getBeginJavaLine(),
						errMsgBuf, ctxt);
			} else {
				javacError = new JavacErrorDetail(fname, lineNum, errNode
						.getStart().getFile(), errNode.getStart()
						.getLineNumber(), errMsgBuf, ctxt);
			}
		} else {
			/*
			 * javac error line number cannot be mapped to JSP page line number.
			 * For example, this is the case if a scriptlet is missing a closing
			 * brace, which causes havoc with the try-catch-finally block that
			 * the code generator places around all generated code: As a result
			 * of this, the javac error line numbers will be outside the range
			 * of begin and end java line numbers that were generated for the
			 * scriptlet, and therefore cannot be mapped to the start line
			 * number of the scriptlet in the JSP page. Include just the javac
			 * error info in the error detail.
			 */
			javacError = new JavacErrorDetail(fname, lineNum, errMsgBuf);
		}
		return javacError;
	}

	/*
	 * Visitor responsible for mapping a line number in the generated servlet
	 * source code to the corresponding JSP node.
	 */
	static class ErrorVisitor extends Node.Visitor {

		// Java source line number to be mapped
		private int lineNum;

		/*
		 * JSP node whose Java source code range in the generated servlet
		 * contains the Java source line number to be mapped
		 */
		Node found;

		/*
		 * Constructor.
		 * 
		 * @param lineNum Source line number in the generated servlet code
		 */
		public ErrorVisitor(int lineNum) {
			this.lineNum = lineNum;
		}

		public void doVisit(Node n) throws JasperException {
			if ((lineNum >= n.getBeginJavaLine())
					&& (lineNum < n.getEndJavaLine())) {
				found = n;
			}
		}

		/*
		 * Gets the JSP node to which the source line number in the generated
		 * servlet code was mapped.
		 * 
		 * @return JSP node to which the source line number in the generated
		 * servlet code was mapped
		 */
		public Node getJspSourceNode() {
			return found;
		}
	}
}
